Repository navigation
fix(native): honour inherits: false on registered custom properties - #412
Open
YevheniiKotyrlo wants to merge 1 commit into
Open
YevheniiKotyrlo wants to merge 1 commit into
YevheniiKotyrlo wants to merge 1 commit into
Conversation
This was referenced Aug 14, 2026
YevheniiKotyrlo
marked this pull request as draft
August 15, 2026 14:35
YevheniiKotyrlo
marked this pull request as ready for review
August 15, 2026 19:13
Contributor
Author
YevheniiKotyrlo
force-pushed
the
fix/non-inheriting-custom-properties
branch
from
October 11, 2026 12:35
adc1691 to
7ffd379
Compare
This was referenced Oct 11, 2026
Open
YevheniiKotyrlo
marked this pull request as draft
October 16, 2026 13:17
YevheniiKotyrlo
marked this pull request as ready for review
October 16, 2026 13:18
This was referenced Oct 16, 2026
Draft
YevheniiKotyrlo
force-pushed
the
fix/non-inheriting-custom-properties
branch
from
October 22, 2026 12:41
7ffd379 to
cdf6e28
Compare
@Property { inherits: false } stops a custom property at the element that declares it (css-properties-values-api-1 §3). The native runtime inherited every custom property, so a descendant resolved a value it must not see. Every Tailwind v4 shadow-* utility composes var(--tw-ring-shadow), so a shadow-none descendant painted its ancestor's ring. The compiler publishes the non-inheriting names (vn) and each registration's initial value (vi), separate from :root's variables, since a declaration wins over a property's default wherever the @Property rule sits. A registration whose initial value depends on the element (em, rem, ch, lh, cq* units, inside calc() or a transform too) is ignored, as browsers do. The inliner folds a single declaration into every consumer, so a non-inheriting property declared once on a class reached elements without that class: a ring-2 element painted shadow-sm's shadow. It now leaves a non-inheriting property to the runtime unless a universal rule declares it, which every element does for itself. At runtime every channel that publishes variables to descendants (a stylesheet rule, an inline vars(), VariableContextProvider) withholds a non-inheriting property, :root no longer reaches one, and the registered initial value is the last rung before the var() fallback. A reload replaces both halves of every registration, retracting a deleted initial value through its observable so mounted elements update, and the jest preset resets the registries between tests. Both registries are pinned to globalThis, like the style collection, so a second copy of the module reads the same ones.
YevheniiKotyrlo
force-pushed
the
fix/non-inheriting-custom-properties
branch
from
October 23, 2026 16:10
cdf6e28 to
0e1b564
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Problem
@property { inherits: false }stops a custom property at the element that declares it (css-properties-values-api-1 §3). Onmainthe native runtime inherits every custom property, so a descendant resolves a value it must not see.Every Tailwind v4
shadow-*utility,shadow-noneincluded, composesvar(--tw-ring-shadow). So ashadow-noneelement inside aring-2 ring-red-500ancestor paints the ancestor's ring.The value reaches descendants through every channel:
vars();VariableContextProvider;:root, which reaches every element;ring-2element also paintsshadow-sm's shadow.Two more defects on
main:@propertyrule'sinitial-valueis written to the:rootslot, so source order decides between a:rootdeclaration and the property's default.Solution
vn) and each registration's initial value (vi), apart from:root's variables (vr).calc()and transforms too) is ignored, as browsers do.assignInheritedVariableswithholds a non-inheriting property on every channel that publishes variables to descendants.varResolverskips:rootfor one, and reads the registered initial value last, before thevar()fallback.injectreplaces both halves of every registration. A deleted initial value is retracted through its observable, so a mounted element updates.globalThis, like the style collection, so a second copy of the module reads the same ones. The jest preset resets them between tests.Tests
src/__tests__/compiler/property.test.ts:vnandvislotssrc/__tests__/native/non-inheriting-variables.test.tsx::root, and*both inlined and at runtimesrc/__tests__/native/vars.test.tsx: an inlinevars().src/__tests__/native/variable-cycles.test.tsx: a cycle through:root.main, 59 of the 113 cases in these files fail. Each of 29 mutations fails its own cases.Verification
On this head, every
ci.ymljob but the iOS build passes on Ubuntu with Node 22.18: build · lint · typecheck ·yarn test1431 passed ·node --test· the web export · no unstaged files · commitlint. On Windows with Node 26,yarn typecheck,yarn lintandyarn testpass too.The iOS dev build, run with Xcode 27.1 where CI pins 26.6, fails on
mainas on this head, before any JavaScript is bundled: Expo'sExpoModulesJSIdoes not build with it (cannot find type '__ObjC::expo' in scope).No issue here or in nativewind/nativewind tracks this; I searched both for
inherits,@propertyandring.Device evidence, before and after: #412 (comment)
Known limits
@propertyrule lightningcss rejects aborts the whole compile, where a browser ignores just that rule: an initial value that does not match the syntax, or a missinginherits. fix(compiler): recover from a rule lightningcss cannot parse #493 recovers from it.calc(1 + 1px)as a<length>and simplifiescalc(3px * sign(1em))to3pxbefore the compiler sees either, so each registers here where Chromium and Firefox ignore the rule.@propertynested in@mediaregisters in browsers while the query matches. lightningcss parses it as an unknown rule, so it is not registered here.@mediaquery, into every consumer. fix(compiler): scope single-definition variable inlining to the declaring block #413 scopes it to the declaring block.Merge order
This overlaps #388, #391, #392, #411, #413, #415, #423, #426, #431, #434, #436, #443, #467, #469, #484, #485, #486, #494, #496, #498, #501 and #503; I'll re-cut whichever lands after the others.